import type { Crypto } from '../../../types/index.ts';

let webCrypto: Crypto | undefined = undefined;

/**
 * Try to get an instance of the Crypto API from the current runtime. Should support Node,
 * as well as others, like Deno, that implement Web APIs.
 */
export function getWebCrypto(): Promise<Crypto> {
  /**
   * Hello there! If you came here wondering why this method is asynchronous when use of
   * `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this
   * synchronous. For example, `generateRegistrationOptions()` and `generateAuthenticationOptions()`
   * become synchronous if we make this synchronous (since nothing else in that method is async)
   * which represents a breaking API change in this library's core API.
   *
   * TODO: If it's after February 2025 when you read this then consider whether it still makes sense
   * to keep this method asynchronous.
   */
  const toResolve = new Promise<Crypto>((resolve, reject) => {
    if (webCrypto) {
      return resolve(webCrypto);
    }

    /**
     * Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
     * support (and Node v20+)
     */
    const _globalThisCrypto = _getWebCryptoInternals.stubThisGlobalThisCrypto();

    if (_globalThisCrypto) {
      webCrypto = _globalThisCrypto;
      return resolve(webCrypto);
    }

    // We tried to access it both in Node and globally, so bail out
    return reject(new MissingWebCrypto());
  });

  return toResolve;
}

export class MissingWebCrypto extends Error {
  constructor() {
    const message = 'An instance of the Crypto API could not be located';
    super(message);
    this.name = 'MissingWebCrypto';
  }
}

// Make it possible to stub return values during testing
export const _getWebCryptoInternals = {
  stubThisGlobalThisCrypto: () => globalThis.crypto,
  // Make it possible to reset the `webCrypto` at the top of the file
  setCachedCrypto: (newCrypto: Crypto | undefined) => {
    webCrypto = newCrypto;
  },
};
